UNPKG

studiocms

Version:

Astro Native CMS for AstroDB. Built from the ground up by the Astro community.

348 lines (284 loc) 9.21 kB
import { User } from 'studiocms:auth/lib'; import { apiResponseLogger } from 'studiocms:logger'; import { Notifications } from 'studiocms:notifier'; import { SDKCore } from 'studiocms:sdk'; import { UserPermissionLevel } from '@withstudiocms/auth-kit/types'; import { AllResponse, createEffectAPIRoutes, createJsonResponse, Effect, genLogger, OptionsResponse, parseAPIContextJson, Schema, } from '../../../../../effect.js'; import { verifyAuthTokenFromHeader } from '../../utils/auth-token.js'; type PermissionRank = 'visitor' | 'editor' | 'admin' | 'owner' | 'unknown'; export class JSONData extends Schema.Class<JSONData>('JSONData')({ rank: Schema.Union( Schema.Literal('owner'), Schema.Literal('admin'), Schema.Literal('editor'), Schema.Literal('visitor'), Schema.Literal('unknown') ), }) {} export const { ALL, DELETE, GET, OPTIONS, PATCH } = createEffectAPIRoutes( { GET: (ctx) => genLogger('studioCMS:rest:v1:users:[id]:GET')(function* () { const [sdk, user, userUtils] = yield* Effect.all([ SDKCore, verifyAuthTokenFromHeader(ctx), User, ]); if (user instanceof Response) { return user; } const { rank } = user; if (rank !== 'owner' && rank !== 'admin') { return apiResponseLogger(401, 'Unauthorized'); } const { id } = ctx.params; if (!id) { return apiResponseLogger(400, 'Invalid form data, id is required'); } const existingUser = yield* sdk.GET.users.byId(id); if (!existingUser) { return apiResponseLogger(404, 'User not found'); } const { avatar, createdAt, email, name, permissionsData, updatedAt, url, username } = existingUser; const existingUserRank = (permissionsData?.rank ?? 'visitor') as PermissionRank; const data = { avatar, createdAt, email, id, name, rank: existingUserRank, updatedAt, url, username, }; const loggedInUser = yield* sdk.GET.users.byId(user.userId); if (!loggedInUser || loggedInUser === undefined) { return apiResponseLogger(400, 'User Error'); } const permissionLevelInput = { isLoggedIn: true, user: loggedInUser, permissionLevel: (loggedInUser.permissionsData?.rank ?? 'visitor') as PermissionRank, }; const userPermissionLevel = yield* userUtils.getUserPermissionLevel(permissionLevelInput); const requiredPerms = () => { switch (existingUserRank) { case 'owner': return UserPermissionLevel.owner; case 'admin': return UserPermissionLevel.admin; case 'editor': return UserPermissionLevel.editor; case 'visitor': return UserPermissionLevel.visitor; default: return UserPermissionLevel.unknown; } }; const isAllowed = userPermissionLevel > requiredPerms(); if (!isAllowed) { return apiResponseLogger(401, 'Unauthorized'); } return createJsonResponse(data); }), PATCH: (ctx) => genLogger('studioCMS:rest:v1:users:[id]:PATCH')(function* () { const [sdk, user, userUtils, notifier] = yield* Effect.all([ SDKCore, verifyAuthTokenFromHeader(ctx), User, Notifications, ]); if (user instanceof Response) { return user; } const { rank } = user; if (rank !== 'owner' && rank !== 'admin') { return apiResponseLogger(401, 'Unauthorized'); } const { id } = ctx.params; if (!id) { return apiResponseLogger(400, 'Invalid form data, id is required'); } const existingUser = yield* sdk.GET.users.byId(id); if (!existingUser) { return apiResponseLogger(400, 'User not found'); } const { permissionsData } = existingUser; const existingUserRank = (permissionsData?.rank ?? 'admin') as PermissionRank; const loggedInUser = yield* sdk.GET.users.byId(user.userId); if (!loggedInUser || loggedInUser === undefined) { return apiResponseLogger(400, 'User Error'); } const permissionLevelInput = { isLoggedIn: true, user: loggedInUser, permissionLevel: (loggedInUser.permissionsData?.rank ?? 'visitor') as PermissionRank, }; const userPermissionLevel = yield* userUtils.getUserPermissionLevel(permissionLevelInput); const requiredPerms = () => { switch (existingUserRank) { case 'owner': return UserPermissionLevel.owner; case 'admin': return UserPermissionLevel.admin; case 'editor': return UserPermissionLevel.editor; case 'visitor': return UserPermissionLevel.visitor; default: return UserPermissionLevel.unknown; } }; const isAllowed = userPermissionLevel > requiredPerms(); if (!isAllowed) { return apiResponseLogger(401, 'Unauthorized'); } const { rank: newRank } = yield* parseAPIContextJson(ctx, JSONData); if (!newRank) { return apiResponseLogger(400, 'Missing field: Rank is required'); } // Prevent privilege escalation: actor cannot assign a rank >= their own const requiredPermsForNewRank = (() => { switch (newRank) { case 'owner': return UserPermissionLevel.owner; case 'admin': return UserPermissionLevel.admin; case 'editor': return UserPermissionLevel.editor; case 'visitor': return UserPermissionLevel.visitor; default: return UserPermissionLevel.unknown; } })(); if (userPermissionLevel <= requiredPermsForNewRank) { return apiResponseLogger(403, 'Forbidden'); } const updateRank = yield* sdk.UPDATE.permissions({ user: id, rank: newRank, }); if (!updateRank) { return apiResponseLogger(400, 'Failed to update rank'); } const updatedUser = yield* sdk.GET.users.byId(id); if (!updatedUser) { return apiResponseLogger(400, 'Failed to get updated user'); } const { avatar, createdAt, email, name, permissionsData: newPermissionsData, updatedAt, url, username, } = updatedUser; const updatedUserRank = (newPermissionsData?.rank ?? 'unknown') as PermissionRank; const data = { avatar, createdAt, email, id, name, rank: updatedUserRank, updatedAt, url, username, }; yield* notifier.sendUserNotification('account_updated', id); yield* notifier.sendAdminNotification('user_updated', username); return createJsonResponse(data); }).pipe(Notifications.Provide), DELETE: (ctx) => genLogger('studioCMS:rest:v1:users:[id]:DELETE')(function* () { const [sdk, user, userUtils, notifier] = yield* Effect.all([ SDKCore, verifyAuthTokenFromHeader(ctx), User, Notifications, ]); if (user instanceof Response) { return user; } const { rank } = user; if (rank !== 'owner' && rank !== 'admin') { return apiResponseLogger(401, 'Unauthorized'); } const { id } = ctx.params; if (!id) { return apiResponseLogger(400, 'Invalid form data, id is required'); } if (id === user.userId) { return apiResponseLogger(400, 'Cannot delete your own account'); } const existingUser = yield* sdk.GET.users.byId(id); if (!existingUser) { return apiResponseLogger(400, 'User not found'); } const { permissionsData } = existingUser; const existingUserRank = (permissionsData?.rank ?? 'admin') as PermissionRank; const loggedInUser = yield* sdk.GET.users.byId(user.userId); if (!loggedInUser || loggedInUser === undefined) { return apiResponseLogger(400, 'User Error'); } const permissionLevelInput = { isLoggedIn: true, user: loggedInUser, permissionLevel: (loggedInUser.permissionsData?.rank ?? 'visitor') as PermissionRank, }; const userPermissionLevel = yield* userUtils.getUserPermissionLevel(permissionLevelInput); const requiredPerms = () => { switch (existingUserRank) { case 'owner': return UserPermissionLevel.owner; case 'admin': return UserPermissionLevel.admin; case 'editor': return UserPermissionLevel.editor; case 'visitor': return UserPermissionLevel.visitor; default: return UserPermissionLevel.unknown; } }; const isAllowed = userPermissionLevel > requiredPerms(); if (!isAllowed) { return apiResponseLogger(401, 'Unauthorized'); } const response = yield* sdk.DELETE.user(id); if (!response) { return apiResponseLogger(400, 'Failed to delete user'); } if (response.status === 'error') { return apiResponseLogger(400, response.message); } yield* notifier.sendAdminNotification('user_deleted', existingUser.username); return apiResponseLogger(200, response.message); }).pipe(Notifications.Provide), OPTIONS: () => Effect.try(() => OptionsResponse({ allowedMethods: ['GET', 'PATCH', 'DELETE'] })), ALL: () => Effect.try(() => AllResponse()), }, { cors: { methods: ['GET', 'PATCH', 'DELETE', 'OPTIONS'] }, onError: (error) => { console.error('API Error:', error); return createJsonResponse({ error: 'Something went wrong' }, { status: 500 }); }, } );